Interview Playbook [Systems Design]
45β60 min Β· you drive, interviewer steers
The single biggest L4 β L5 difference: at L5 you propose the agenda, make decisions, and justify trade-offs without being dragged through each step. Run this sequence out loud, announce transitions ("requirements look solid β let me sketch the API"), and check in at each boundary. Budgets assume 45 minutes of design time.
1
Requirements β functional, then non-functional
~5 min
"Let me scope this to 3 core features, then pin the scale and latency constraints."
- Functional: list 3β4 core user flows ("post a tweet, view a feed, follow"). Explicitly cut the rest: "I'll skip search and DMs unless you want them."
- Non-functional β these drive the whole design: how many users? read-heavy or write-heavy? latency target? consistency vs availability β which matters more here?
- Ask "what's the read:write ratio?" β it decides caching, replication, fan-out.
- Write requirements down (top of the doc/whiteboard). You'll return to them in deep dives.
- L5 move: propose the scale yourself ("assume 10M DAU?") instead of waiting to be told.
2
Estimate β only what changes the design
~3 min
"10M DAU Γ 10 reads/day β 100M reads/day β ~1,200 QPS average, maybe 5k peak. One DB with replicas handles that."
- QPS: requests/day Γ· 100,000 β average QPS (86,400 sec/day, round to 100k). Peak = 3β5Γ.
- Storage: rows Γ bytes/row Γ retention. Only compute if data size drives sharding/blob decisions.
- End with a conclusion, not a number: "so this fits in one Postgres with read replicas" or "this write volume forces us to partition."
- If the math doesn't affect any decision, say so and skip it. Interviewers respect that.
"One endpoint per core flow. GET /feed?cursor=... β cursor pagination, offset breaks at depth."
- Write one endpoint per functional requirement: method, path, params, response shape. REST unless there's a reason (real-time β WebSocket, flexible clients β GraphQL).
- Core entities + fields + relationships: User, Tweet, Follow. Note the indexes your queries need.
- This is your full-stack home turf β show it: cursor vs offset pagination, idempotency keys on POSTs, what the client caches, auth in a sentence (sessions vs JWT).
- Don't design 15 endpoints. Core flows only; say the rest is mechanical.
4
High-level design β boxes that satisfy every requirement
~10 min
"Client β CDN β load balancer β stateless API servers β cache β database. Let me walk a write end to end."
- Draw the default skeleton (above), then adapt. Stateless app tier = horizontal scaling for free.
- Walk one read and one write through the diagram out loud, end to end. This catches gaps fast.
- Map every functional requirement to a path through the boxes. Unmapped requirement = hole.
- Anything slow or non-critical (notifications, fan-out, thumbnails) β queue + async worker.
- Keep it simple here. Depth comes next β say "I'm staying high level, then I'll deep-dive where it's interesting."
5
Deep dives β where L5 is won or lost
~15 min
"The interesting problem here is feed fan-out. Two options: fan-out on write vs on read. Given the read ratio, I'd choose..."
- Pick 2β3 dives yourself, driven by the non-functional requirements from step 1. Don't wait to be prompted.
- Format every dive the same: problem β 2 options β trade-off β decision β why. Naming a technology without the why is an L4 tell.
- Common dives: scaling reads (cache + replicas), scaling writes (partition/queue), real-time delivery (WebSocket vs SSE vs polling), contention (two users, one seat β locking vs atomic ops), hot keys/celebrities, failure handling (retries + idempotency).
- Quantify when you can: "cache hit rate ~90% cuts DB load 10Γ."
- Follow the interviewer's nudges β a hint means they want depth there. Engage immediately.
6
Wrap β bottlenecks & evolution
~5 min
"Single points of failure: the DB primary β I'd add failover. At 10Γ scale, the first thing to break is..."
- Name the weakest link and its fix (replica promotion, cache stampede protection, DLQ for the queue).
- One sentence on observability: metrics on latency p99, error rates, queue depth; alerts on saturation.
- 10Γ question: say what breaks first at 10Γ scale and the upgrade path. Shows headroom thinking.
- Revisit the requirements list β confirm each is satisfied. Close the loop.
Clarifying questions to keep in your pocket
How many users / DAU?
Read:write ratio?
Latency target β p99?
Consistency or availability when forced to pick?
Global or single region?
Data retention β forever?
Real-time or is eventual fine?
Which features are in scope?
Mobile + web clients?
Can I assume managed services (S3, SQS)?
Common traps
- Boxes before requirements. A perfect design for the wrong problem scores zero.
- Name-dropping without why. "I'd use Kafka" is nothing; "a queue decouples the spike from the DB, Kafka because we need replay" is the bar.
- Uniform shallowness. Covering ten topics at 1-inch depth reads L4. Two dives at real depth reads L5.
- Premature scale. Don't shard a database that fits on one machine. Start simple, scale when the numbers demand it β and say that's what you're doing.
- Ignoring hints. Interviewer nudges are the rubric leaking. Take them.
- Silence while drawing. Narrate every box as you add it.
What they're scoring at L5
- Ownership: you drove start to finish; the interviewer only steered.
- Requirements β design traceability: every choice ties back to a stated constraint.
- Trade-off fluency: two options and a justified pick, every time.
- Quantified reasoning: rough numbers that lead to decisions.
- Depth on demand: at least two areas where you go genuinely deep.
- Pragmatism: simplest thing that meets the requirements, with a scaling path.
Full-stack edge β use it
- API contract details (pagination, idempotency, errors) β most backend candidates skip these; you shouldn't.
- Client-side story: optimistic updates, browser caching, stale-while-revalidate.
- Real-time delivery trade-offs (WebSocket vs SSE vs polling) end to end, client included.
- When infra depth runs out, say so honestly and reason from first principles β that beats bluffing.
Technology overview β Sharding
- What: split one dataset across nodes by a key (hash or range on
user_id).
- Use when: writes or data size exceed one primary β not before. Say that out loud.
- Trade-offs: cross-shard queries become fan-outs; joins and transactions get hard; hot keys (celebrities) skew load; resharding is painful β pick the key carefully, and colocate rows that commit together so writes stay single-shard (no 2PC).
SQL vs NoSQL
- SQL (Postgres, MySQL): joins, ACID transactions, strong consistency. The default β most systems fit one instance + read replicas.
- NoSQL (DynamoDB, Cassandra): key-based access at massive write scale, built-in horizontal scaling. Cost: no joins, limited transactions, query patterns fixed up front.
- Redis: in-memory KV β caching, sessions, counters, rate limits. Sub-ms reads, but RAM-priced and lossy on crash.
Durable message queues
- What: persist work between producer and consumer, decoupling spikes from the DB.
- Kafka: replayable append-only log, huge throughput β event streams, fan-out, analytics pipelines.
- SQS / RabbitMQ: simpler task queues with per-message ack β background jobs, emails, thumbnails.
- Trade-offs: at-least-once delivery β consumers must be idempotent; ordering only per partition; adds latency and ops surface.
Object storage (S3)
- What: cheap, ~11-nines-durable blob store for images, video, logs, backups.
- Use when: anything large or unstructured β store the blob in S3, its URL in the DB, serve via CDN with presigned URLs for uploads.
- Trade-offs: higher latency than a DB, no queries or partial updates β wrong for small hot records.
Don't forget β high-value patterns to reach for
CDC + outbox β commit to DB, then emit to Kafka
- Problem (dual write): you need to update the DB and publish an event (Kafka). Doing both directly isn't atomic β DB commits but the publish fails (or vice versa) and the two diverge.
- Outbox: in the same transaction as the business write, insert the event into an
outbox table. One atomic commit β data and intent-to-publish succeed or fail together.
- CDC relay: a separate process (Debezium tailing the WAL, or a poller) reads committed outbox rows and publishes to Kafka. DB is the single source of truth; the event is guaranteed to ship.
- Cost: at-least-once delivery β consumers must be idempotent; small publish lag.
- Line: "To dodge the dual-write problem I write an outbox row in the same txn and a CDC relay publishes to Kafka β atomic commit, at-least-once, consumers dedupe."
Load balancer + API gateway β the edge
- Load balancer: spreads traffic across stateless app servers with health checks (drops dead nodes). L4 (TCP, fast) vs L7 (HTTP, can route by path/header). Stateless tier is what lets it scale horizontally.
- API gateway: the single client entry point and policy layer β auth, rate limiting, routing to services, TLS termination, request shaping. In microservices it fronts many services so clients hit one endpoint.
- Where they sit:
Client β CDN β LB β API gateway β app/services. LB = spread load, gateway = policy + routing; often merged at the edge.
- Line: "LB load-balances a stateless app tier with health checks; the gateway handles auth, rate limiting, and routing so services stay focused on business logic."
Connection pooling β short
- What: reuse open TCP connections instead of one per request β amortizes TCP + TLS handshake RTTs.
- Use when: repeated calls to the same long-lived backend (appβDB, appβRedis). Size to the backend's connection cap (Postgres limits are low β PgBouncer).
- Note: avoids the handshake, not the round trip β say that. Skip for HTTP/2-3, serverless, or many distinct hosts.
- Line: "Pool connections to Redis/DB to amortize handshake cost, sized to the connection limit, plus pipelining to batch round trips."
ACID + shard to colocate transactions
- Postgres gives you ACID: atomic multi-row commits with real isolation. That's why booking/payment logic stays simple β flip N tickets and write the booking in one transaction, all-or-nothing.
- Shard so a transaction stays on one shard. Pick the key so rows that commit together colocate β e.g. shard by
eventId so every ticket for an event lives on the same shard.
- Why it matters: a cross-shard transaction needs distributed commit (2PC) β slow, complex, and a new failure mode. Single-shard transactions keep ACID cheap.
- Line: "I'll shard by the entity we transact against so writes stay single-shard and I never need 2PC."
Idempotent writes β safe retries
- Client generates a UUID per logical write, sent as
Idempotency-Key. The client owns it because it needs to be stable across retries.
- Server stores
key β response on first execution; a retry with the same key returns the stored response instead of re-running the write.
- Why: networks retry on timeout β without this you double-charge or double-book. Essential on any non-idempotent POST; forward the key to the payment provider so it dedupes the charge too.
- Line: "Every mutating call carries a client UUID; a retry returns the same response, so a timeout never double-applies."
Cache strategy + stampede protection
- Cache-aside (default): app reads cache, on miss reads DB and populates. On write, update the DB then invalidate the key. Simple, resilient to cache loss.
- Write-through / write-back: writes go via the cache β stronger read consistency (through) or faster writes at durability risk (back). Only reach for these with a reason.
- Stampede (thundering herd): a hot key expires and thousands of misses hit the DB at once. Fix with jittered TTLs, a single-flight lock (one request recomputes, others wait), and refresh-ahead before expiry.
- Line: "Cache-aside with jittered TTLs and a single-flight lock, so one hot key expiring can't stampede the DB."
Consistent hashing
- What: place keys and nodes on a ring; a key maps to the next node clockwise. Adding/removing a node moves only ~1/N of keys, not the whole keyspace.
- Use when: sharding caches or DBs across a node set that changes β autoscaling, failures, adding capacity. Virtual nodes smooth out uneven load.
- Why it matters: naive
hash % N remaps almost everything when N changes, cold-flushing every cache at once.
- Line: "Consistent hashing so adding a shard remaps ~1/N of keys instead of reshuffling the whole ring."
Replica lag + read-after-write
- Setup: leader takes writes, replicas stream asynchronously and lag by ms to seconds. Great for scaling reads, but the copy is a moment in the past.
- Read-after-write trap: user writes, immediately reads a lagging replica, and doesn't see their own change. Fix by routing that user's reads to the leader briefly, or a version/timestamp token, or sticky routing.
- Correctness rule: never authorize a write off a replica (e.g. "is this seat free?"). That read must hit the leader with a row lock β replicas are for display, not for gating writes.
- Line: "Replicas lag, so I route read-after-write to the leader and never make a write decision from a replica."
Backpressure + dead-letter queue
- Backpressure: a queue decouples spikes, but consumers fall behind. Bound the queue and shed/throttle producers so you degrade instead of OOM-ing. Queue depth is a key metric + alert.
- Retries + DLQ: transient failures β retry with exponential backoff; after N tries a poison message goes to a dead-letter queue for inspection instead of blocking the pipeline.
- Idempotent consumers: delivery is at-least-once, so processing the same message twice must be safe.
- Line: "Bounded queue with backoff retries and a DLQ for poison messages; consumers idempotent because delivery is at-least-once."
Sagas + compensating actions β when one transaction won't fit
- What: a workflow that spans services or an external system (booking + Stripe, order + inventory + shipping) can't sit in one ACID transaction. Break it into a sequence of local transactions, each with a compensating action that semantically undoes it if a later step fails. This is the alternative to 2PC.
- Compensation is a semantic undo, not a rollback:
refund reverses a charge, release reverses a reservation, cancel reverses a shipment. It must be idempotent, since it may be retried.
- Ticketmaster example: reserve seats (compensate: release) β charge Stripe (compensate: refund) β confirm booking. Charge fails β release the seats; a crash after charge but before confirm β retry confirm, or refund if it can't complete.
- Choreography vs orchestration: choreography = each step emits an event (via the outbox) and the next reacts β decoupled, simple; orchestration = a central coordinator drives the steps β clearer for complex flows, one place to reason about state.
- Trade-off (say it): a saga has no isolation β intermediate states are visible (a seat is reserved-but-not-paid). You design for that window explicitly, which is exactly what the reservation TTL /
reserved_until is for. Consistency is eventual.
- Line: "The booking-plus-payment flow is a saga, not a distributed transaction: reserve, charge, confirm, each with a compensating action β release and refund β and idempotent so retries are safe."